Skip to content

feat(reid): appearance ReID association via standalone reid package - #511

Open
AlexBodner wants to merge 101 commits into
developfrom
feat/core/reid-consume-reid-package
Open

feat(reid): appearance ReID association via standalone reid package#511
AlexBodner wants to merge 101 commits into
developfrom
feat/core/reid-consume-reid-package

Conversation

@AlexBodner

@AlexBodner AlexBodner commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds optional appearance-ReID to BoT-SORT, sourcing the model stack from the standalone reid package. Trackers ships only NumPy association glue (ReIDEncoder, FeatureBank, appearance_similarity, extract_detection_embeddings) plus BoT-SORT appearance–IoU fusion. Encoder weights, preprocessing, catalog, and gallery evaluation live in reid and are never vendored.

This branch is merged with current develop (including McByte) and is independent of the earlier stacked ReID PRs (#480/#501/#502).

Added things

  • Deps: trackers[reid] optional extra → reid>=0.1.0.dev0,<0.2 (PyPI). reid.* in mypy overrides; uv.lock updated.
  • Association glue (src/trackers/core/reid/, torch-free): ReIDEncoder, FeatureBank (BoT-SORT EMA L2 before/after blend), appearance_similarity, extract_detection_embeddings.
  • Lazy import: CLI loads reid.ReIDModel at use time (same pattern as detection); importing BoT-SORT stays free of reid / torch (isolation test).
  • BoT-SORT wiring: appearance–IoU fusion, per-track feature bank, params reid_model, reid_ema_alpha, appearance_threshold, proximity_threshold.
  • CLI: --tracker.reid.{enable,model,device,architecture} on trackers track (BoT-SORT only).
  • Tests: association / fusion / CLI unit tests (no --extra reid required), @integration smoke with a real reid.ReIDModel, isolation test.
  • Docs / notebook: docs/learn/reid.md (threshold guide + MOT17/SoccerNet association-local plots), docs/api/reid.md, install notes, BoT-SORT Appearance ReID section; notebooks/eval_trackers_reid.ipynb installs from PyPI.
  • CI: unit (ci-tests.yml) syncs without --extra reid; integration (ci-integrations.yml) keeps --extra reid.

Pre-merge checklist

  • Publish reid to PyPI (0.1.0.dev0).
  • Swap the git pin in pyproject.toml to a PyPI range (reid>=0.1.0.dev0,<0.2) and refresh uv.lock.
  • Resolve merge conflicts with develop (McByte / mask extras) so PR CI can run.
  • Confirm integration CI is green with --extra reid (smoke should run, not skip).
  • Refresh MOT17 / SoccerNet appearance-threshold docs figures (association-local sampling).
  • pre-commit.ci green.

@AlexBodner
AlexBodner requested a review from SkalskiP as a code owner July 21, 2026 14:01
AlexBodner and others added 3 commits July 21, 2026 11:07
Add the optional trackers[reid] extra (git-pinned roboflow-reid during
review) plus the lazy trackers._reid boundary that resolves reid.ReIDModel
on demand. Ship association-only, torch-free modules under
trackers.core.reid: the ReIDEncoder protocol, FeatureBank (per-track EMA),
and appearance_similarity / extract_detection_embeddings.

The model stack (encoder, weights, preprocessing, catalog, gallery eval)
lives in the standalone reid package and is never vendored.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add appearance-IoU fusion (botsort/fusion.py) and wire it into BoT-SORT's
first and unconfirmed association stages, gated by proximity (standard IoU)
and appearance thresholds. Tracklets gain an optional per-track feature
bank; matched high-confidence detections update it. reid_model,
reid_ema_alpha, appearance_threshold, and proximity_threshold are new
BoTSORTTracker params; reid_model is excluded from CLI reflection.

Add --tracker.reid.{enable,model,device,architecture} CLI flags routed
through trackers._reid to reid.ReIDModel.from_pretrained. Importing BoT-SORT
stays torch-free (asserted by an isolation test). Add association/fusion/CLI
unit tests and a reid-backed integration smoke; docs and mkdocs nav cover
the association-only surface and link the model/eval stack out to reid.

CI installs the reid extra (unfrozen while the dep is a git pin).

Co-authored-by: Cursor <cursoragent@cursor.com>
- FeatureBank.update now L2-normalizes the incoming embedding and the
  resulting EMA, keeping the stored feature on the unit hypersphere
  (matches upstream BoT-SORT STrack.update_features). reid returns raw
  embeddings; normalization happens in the bank. Docstrings/tests updated.
- Vectorize appearance._l2_normalize_rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
@AlexBodner
AlexBodner force-pushed the feat/core/reid-consume-reid-package branch from 5afd3b9 to fbbe664 Compare July 21, 2026 14:15
@AlexBodner AlexBodner changed the title feat(reid): appearance ReID association via standalone roboflow-reid package feat(reid): appearance ReID association via standalone reid package Jul 21, 2026
AlexBodner and others added 6 commits July 21, 2026 12:04
…tion.

Ports eval_trackers_reid from the reid-training branch with standalone reid imports and Colab-friendly private git installs for both trackers and re-ID.

Co-authored-by: Cursor <cursoragent@cursor.com>
Exclude the default dev group from build/docs sync, authenticate git in
integration/pytest workflows, and fix pre-commit failures in the ReID
benchmark notebook and codespell config.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move the private git dependency to the reid extra only, avoid uv run
re-syncing the dev group in build/docs jobs, and fix notebook JSON for
pre-commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
State what trackers.core.reid covers positively, defer model/eval to the
reid package, and drop subtractive architecture prose. Align install extra
wording with develop (no roboflow-reid in prose).

Co-authored-by: Cursor <cursoragent@cursor.com>
AlexBodner and others added 2 commits July 21, 2026 12:27
Add MOT17 val with/without ReID reference scores to the ReID API page and align the BoT-SORT optional ReID section with trackers docs style.

Co-authored-by: Cursor <cursoragent@cursor.com>
Load frames by MOT index, use per-sequence FPS, re-encode to H.264 for notebook/Colab playback, and embed at the combined panel width.

Co-authored-by: Cursor <cursoragent@cursor.com>
AlexBodner and others added 7 commits July 22, 2026 10:48
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Inline the optional reid import in the CLI like detection/tune, and keep
all BoT-SORT ReID coverage in one test module.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 27 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

notebooks/plot_reid_appearance_distances.py:18

  • This example is not runnable: --dataset both enters the SoccerNet branch, which exits unless --soccernet-root is supplied. Include the required root here, as in the SoccerNet-only example.
  python notebooks/plot_reid_appearance_distances.py --dataset both

Comment thread src/trackers/core/botsort/tracker.py Outdated
AlexBodner and others added 7 commits August 13, 2026 10:14
…sume-reid-package

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	docs/evaluations/results.md
#	docs/guides/reid.md
#	docs/llms.txt
#	mkdocs.yml
#	src/trackers/core/botsort/tracker.py
#	src/trackers/scripts/track.py
#	tests/scripts/test_track.py
#	uv.lock
The CLI rewrite in #413 replaced scripts/track.py with cli/track.py and swapped
argparse for jsonargparse over generated dataclasses, so the argparse-based
--tracker.reid.* flags had no file left to live in.

Appearance loading now travels in a ReIDOptions dataclass registered alongside
DetectionOptions, giving --reid.model and friends plus --config YAML support for
free. The encoder is built in _init_tracker and injected as the reid_model
keyword, mirroring how iou_variant becomes the iou keyword.

reid_ema_alpha, appearance_threshold and proximity_threshold now reach the CLI
straight from the BoTSORTTracker signature via _tracker_parameter_union, so
their flag plumbing is deleted rather than ported. reid_model joins
mask_manager in _EXCLUDED_TRACKER_PARAMETERS, which lets the TrackerParameters
name check added for it disappear from core/base.py: excluding CLI-invisible
constructor args is the CLI's job, and it already had a home for them.

Error cases keep their previous messages and exit codes. Selecting a tracker
without encoder support is still rejected, but the registry answers that now
instead of a hardcoded "botsort" comparison.

Co-authored-by: Cursor <cursoragent@cursor.com>
…sume-reid-package

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	README.md
Co-authored-by: Cursor <cursoragent@cursor.com>
… file

trackers.core.reid is shared association machinery, not BoT-SORT's, but
FeatureBank and appearance_similarity were only covered inside
test_botsort_reid.py. The file name implied a coupling the package explicitly
denies, and it buried the pure-utility tests among tracker integration ones.

Each of the two now sits beside the module it covers, following the flat
test_botsort_*.py naming already used in tests/core. What stays behind is
genuinely BoT-SORT specific: the import guard, the appearance-IoU fusion gates
from botsort/fusion.py, and the tracker integration tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
…code

The docs tell you to calibrate appearance_threshold on your own footage rather
than inherit 0.25 from the paper, then leave you to implement the measurement.
The one working implementation sat in a docs-figure script, where nobody who
installs trackers can reach it, and getting it right is not obvious: the pairs
have to be association-local and the per-sequence quota has to be equal, or one
crowded sequence decides the answer.

trackers.core.reid.thresholds now owns the sampling, the separability metric and
both figures, so the script keeps only what is genuinely dataset-specific,
reading MOT-format ground truth and encoding crops (668 to 383 lines). The
sampler is a straight move, verified pair-for-pair against the old one on
synthetic sequences across four gap bands.

Deliberately no suggest_threshold(). A threshold trades same-ID matches against
different-ID collisions and only the caller knows which costs them more, so
rates_at() reports both sides and the plots mark the thresholds worth comparing,
labelled by role — (selected) against (default) — rather than by library, since
0.25 is the default both here and in BoT-SORT. rates_at() accepts a pair at or
below the threshold, matching fuse_botsort_reid_association, which drops
appearance only once the distance exceeds it.

matplotlib joins the reid extra and is imported inside the plot functions, so the
tracking path never pays for it. A test pins that.

Co-authored-by: Cursor <cursoragent@cursor.com>
The ReID guide explains what appearance association does and shows the numbers,
but a reader who wants to trust a threshold on their own footage still has to
assemble the whole pipeline: get MOT17, run a detector, run the tracker twice,
then measure. This notebook does that end to end in Colab, so the guide's advice
to calibrate is something you can execute rather than just read.

It merges the two half-answers that existed. The version in roboflow/notebooks
had the Colab-first setup and the side-by-side BoT-SORT comparison but no
threshold analysis; the one in this repo had the analysis but assumed a local
checkout with data already on disk. Detections are computed once and shared
between the two tracker runs, so the only difference between them is ReID.

The analysis cells call trackers.core.reid.thresholds rather than redefining the
sampler, which is why the utility landed first: the notebook stays a thin
narrative over library code, and its figures cannot drift from the guide's.

Co-authored-by: Cursor <cursoragent@cursor.com>
@AlexBodner
AlexBodner force-pushed the feat/core/reid-consume-reid-package branch from 9a1d848 to 8153c87 Compare August 13, 2026 18:03
@Borda
Borda requested a balanced review from Copilot August 13, 2026 18:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 32 changed files in this pull request and generated no new comments.

Suppressed comments (6)

src/trackers/core/reid/thresholds.py:327

  • An empty but shape-valid dataset leaves indexes empty, after which _draw_distances calls _split_quota(..., 0) and raises ZeroDivisionError instead of the documented ValueError. Reject zero rows before building the sequence indexes.
    unique_sequences = np.unique(sequence_ids)

src/trackers/core/reid/thresholds.py:412

  • This catches every ValueError from sampling, not only the expected “no pairs in this band” case. Invalid gap bands, mismatched array lengths, and non-finite embeddings are consequently hidden as omitted bands or an empty sweep. Validate shared inputs outside the loop and/or use a dedicated no-pairs exception so genuine input errors still propagate.
        except ValueError:
            continue

src/trackers/core/reid/thresholds.py:330

  • The API describes these values as identity and sequence labels, but coercing them to int rejects normal string labels and can merge distinct non-integer labels (for example, 1.2 and 1.8). Preserve the original hashable sequence values here and identity values in _SequenceIndex rather than coercing both to integers, or explicitly validate and document integer-only inputs.
        int(sequence): _SequenceIndex(np.flatnonzero(sequence_ids == sequence), ids, frame_ids)
        for sequence in unique_sequences

src/trackers/cli/track.py:746

  • architecture is documented as requiring model, but this predicate ignores it. Passing only --reid.architecture therefore bypasses _load_reid_model, silently runs geometry-only tracking, and never raises the existing validation error. Treat a non-None architecture as a ReID request so that validation executes.
    return reid.enable or reid.model is not None

docs/cookbooks/how-to-add-reid-to-trackers.ipynb:226

  • For a zero-based detection file, offset is -1, but max(offset, 0) turns it back into 0; frame 0 therefore remains frame 0 and never matches the tracking loop, which starts at 1. Subtract the signed offset directly so both zero- and one-based files align to frame 1.
                "        by_frame.setdefault(frame - max(offset, 0), []).append([x1, y1, x2, y2, score])\n",

src/trackers/core/reid/thresholds.py:208

  • pairable_ids only means an identity has multiple crops overall, not that it has a valid partner in the requested gap band. When a selected identity/anchor has no partner, the whole draw is rejected and another identity is selected, so accepted samples are weighted by each identity’s success rate; dense tracks can dominate despite the documented uniform-per-identity sampling. Build the candidate identities and anchors for the active gap band first, then sample uniformly from valid candidates.

This issue also appears in the following locations of the same file:

  • line 327
  • line 329
  • line 411
    if not index.pairable_ids:
        return None
    slots = index.slots_by_id[index.pairable_ids[int(rng.integers(len(index.pairable_ids)))]]
    anchor = int(slots[int(rng.integers(len(slots)))])
    window = index.window(index.frames[slots], int(index.frames[anchor]), minimum_frame_gap, maximum_frame_gap)
    partner_slot = _pick_in_window(rng, window)

Borda and others added 15 commits August 13, 2026 23:22
…cept, fix _reid_requested, preserve import errors

[resolve group] PR #511 — items 9, 13, 14, 16, 32

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
…rmed-track fusion

[resolve group] PR #511 — items 22, 23, 24

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
[resolve group] PR #511 — item 25

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
[resolve group] PR #511 — items 12, 17, 26, 27, 28

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
[resolve group] PR #511 — item 31

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
…n histogram xlim

[resolve group] PR #511 — items 15, 18, 33

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Add extract_ground_truth_embeddings beside extract_detection_embeddings. It walks {sequence}/gt/gt.txt against img1, drops ignore-flagged rows, optionally keeps MOT classes, and renumbers identities across sequences.

Re-export it from trackers.core.reid. Delete notebooks/plot_reid_appearance_distances.py and point the cookbook, API page, and guide at the loader.

Update the MOT17 and SoccerNet gap-sweep PNGs. Align the MOT17 rate table (1.6%, 31.8%, 28.7%) and the SoccerNet different-ID range (44% to 51%).

Co-authored-by: Cursor <cursoragent@cursor.com>

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
…iden reid docstring scope

[resolve group] PR #511 — items 19, 20, 29, 30

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
…age' into feat/core/reid-consume-reid-package
[resolve group] PR #511 — Step 9 lint/QA gate

---
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
…com/roboflow/trackers into feat/core/reid-consume-reid-package

# Conflicts:
#	notebooks/plot_reid_appearance_distances.py
#	tests/core/test_reid_appearance.py
Fusion is numpy-only association math over track-detection similarity
matrices, so it belongs in the shared ReID association layer next to
appearance_similarity and FeatureBank rather than under core/botsort.
This makes fusion methods reusable by any tracker.

Pure relocation: no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
thresholds.py was hard to follow for its size. Same behaviour throughout —
sampled distances verified identical to the previous implementation across
4 seeds x 7 gap bands (224k distances) and across 15 identity-label dtypes
(ints, floats, float32, strings, bytes, bools, datetime64, object arrays,
None and NaN).

- roc_auc: replace the searchsorted rank arithmetic with the pairwise
  definition it documents. Note the O(n*m) scaling inline.
- collapse the same-ID/different-ID sampling helpers into
  _SequenceIndex.get_candidates / get_anchor_groups, dropping the callback
  dispatcher, its TypeVar and two type aliases.
- fold the four window helpers into one method returning positions, which
  removes the slot/position duality the comments used to explain.
- index tracks by position (tracks / track_of) rather than looking a track
  up by its identity label while drawing. Labels are hashed once, when the
  index is built, so labels that never compare equal to themselves — NaN
  from a pandas column with missing ids — keep working. Covered by a test.
- drop _scalar_key. Its job was to stop int(identity) merging non-int
  labels; using the label directly does that, and unhashable labels still
  raise TypeError from the dict itself.
- inline the matplotlib import at both plot functions instead of a module
  returning _pyplot() helper, matching core/masks/sam.py and cutie.py.
- name things the way the rest of core/ does: "candidate" over the invented
  "partner", get_* for methods that compute a collection.
- drop the dead maximum_attempts_per_pair parameter; make the sweep's
  percentile band a real parameter instead of a module constant.
- remove comments that restated their own docstrings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants